CSS 层叠层级(@layer)指南

这是您全面了解 CSS 层叠层的指南,CSS 层叠层是一项 CSS 功能,允许我们明确定义具有特定优先级的层,从而在项目中完全控制哪些样式具有优先级,而无需依赖特定性技巧或 !important。本指南旨在帮助您全面了解层叠层的用途、何时以及为何选择使用它们、当前的支持级别以及使用它们的语法。

快速示例

/* establish a layer order up-front, from lowest to highest priority */
@layer reset, defaults, patterns, components, utilities, overrides;

/* import stylesheets into a layer (dot syntax represents nesting) */
@import url('framework.css') layer(components.framework);

/* add styles to layers */
@layer utilities {
  /* high layer priority, despite low specificity */
  [data-color='brand'] { 
    color: var(--brand, rebeccapurple);
  }
}

@layer defaults {
  /* higher specificity, but lower layer priority */
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

简介:什么是层叠层?

CSS 层叠层旨在解决 CSS 中的复杂问题。让我们先看看 主要问题 以及层叠层如何解决它。

问题:特异性冲突升级

我们许多人都遇到过这样的情况:由于选择器冲突,需要覆盖代码中其他部分(或第三方工具)的样式。多年来,作者们开发了多种“方法论”和“最佳实践”来避免此类情况——例如“所有选择器仅使用单一类名”。这些规则通常更侧重于避免级联,而非利用级联

管理级联冲突和选择器特异性常被视为CSS中较难——或至少更令人困惑——的方面之一。这可能部分是因为其他语言很少将级联作为核心特性,但更重要的是,原始级联机制高度依赖于启发式规则(即代码中内置的经验性猜测或假设),而非为网页作者提供直接明确的控制权。元素周期表

例如,选择器特异性——我们与级联的主要交互方式——基于这样一个假设:更窄范围的样式(如仅使用一次的ID)比更通用且可重用的样式(如类和属性)更重要。也就是说:选择器有多“具体”。这是一个合理的猜测,但并非完全可靠的规则,这会引发一些问题:

  • 它将“选择元素”的操作与“优先级规则集”的操作结合在一起。
  • 解决特异性冲突的最简单方法是通过添加本不需要的选择器来升级问题,或者(天啊)扔出 !important 手榴弹
.overly#powerful .framework.widget {
  color: maroon;
}

.my-single_class { /* add some IDs to this ??? */
  color: rebeccapurple; /* add !important ??? */
}

解决方案:层级级联提供控制

层级级联使 CSS 作者能够更直接地控制级联过程,从而构建更具意图的级联系统,而无需过多依赖与选择相关的启发式假设。

通过使用 @layer 规则和分层 @import,我们可以建立自己的 层叠层级——从低优先级的样式(如重置和默认样式)开始,经过主题、框架和设计系统,一直到高优先级的样式(如组件、实用工具和覆盖样式)。特异性仍适用于同一层内的冲突,但层与层之间的冲突始终通过使用更高优先级的层样式来解决。

@layer framework {
  .overly#powerful .framework.widget {
    color: maroon;
  }
}

@layer site {
  .my-single_class {
    color: rebeccapurple;
  }
}

这些层是按顺序排列和分组的,因此它们不会像特异性和重要性那样升级。层级序列不是像选择器那样累积的。添加更多层不会使某物更重要。它们也不是像重要性那样二进制的——突然跳到堆栈顶部——或像z-index那样编号,我们不得不猜测一个大数字(z-index: 9999999?)。事实上,默认情况下,分层样式比未分层样式更不重要

@layer defaults {
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

层级在级联中的位置

级联是一系列步骤(算法)用于解决样式冲突。

html { --button: teal; }
button { background: rebeccapurple !important; }
.warning { background: maroon; }
<button class="warning" style="background: var(--button);">
  what color background?
</button>

添加级联层后,这些步骤为:

 

选择器特异性只是层叠中的一个小部分,但它是我们最常交互的步骤,通常也被用来泛指整体层叠优先级。人们可能会说!important标志或style属性“增加了特异性”——这是快速表达样式在层叠中优先级更高的方式。由于级联层直接位于特异性之上,因此可以将其视为与 ID 选择器类似的更强大步骤。

然而,CSS 级联层也使得我们必须全面理解 !important 在级联中的作用——不仅作为“增加特异性”的工具,更是作为平衡关注点的系统。

!important 的起源、上下文和层级被颠倒了!

作为网页作者,我们通常将 !important 视为提升特异性以覆盖内联样式或高度特异性选择器的方式。这种用法在大多数情况下可行(如果你能接受特异性提升),但它忽略了 !important 作为级联系统中核心功能的真正目的。

重要性并非仅仅为了增强功能,而是为了在各种相互竞争的利益之间实现平衡。

重要性的起源

一切都始于起源,即一种风格在网络生态系统中的来源。CSS中有三个基本的起源:

  • 浏览器(或用户代理)
  • 用户(通常通过浏览器设置)
  • 网页作者(就是我们!)

浏览器为所有元素提供可读的默认样式,然后用户设置其偏好,最后我们(作者)为网页提供预期设计。因此,默认情况下,浏览器具有最低优先级,用户偏好覆盖浏览器默认样式,而我们能够覆盖所有内容。

但 CSS 的创建者非常明确地指出,我们实际上不应拥有最终决定权:

如果出现冲突,用户应拥有最终决定权,但同时也应允许作者附加样式提示。

— Håkon Lie(强调部分)

因此,!important 提供了浏览器和用户在关键时刻重新获得优先级的方式。当在样式中添加 !important 标志时,会创建三个新层级——且顺序被颠倒!

  1. !important 浏览器样式(最强大)
  2. !important 用户偏好
  3. !important 作者样式
  4. 普通作者样式
  5. 普通用户偏好
  6. 普通浏览器样式(最弱)

对于我们来说,添加 !important 不会带来太大变化——我们的重要样式与普通样式非常接近——但对于浏览器和用户而言,这是一个非常强大的工具,用于重新获得控制权。浏览器默认样式表包含一些我们无法覆盖的重要样式,例如:

iframe:fullscreen {
  /* iframes in full-screen mode don't show a border. */
  border: none !important;
  padding: unset !important;
}

尽管大多数主流浏览器已使上传实际用户样式表变得困难,但它们均提供用户偏好设置:一个用于定义特定用户样式的图形界面。在此界面中,始终有一个复选框供用户选择是否允许网站覆盖其偏好设置。这与在用户样式表中设置 !important 的效果相同:

Screenshot of user font preferences.

重要上下文

同样的基本逻辑也适用于层叠中的上下文。默认情况下,主文档(轻量级 DOM)中的样式会覆盖嵌入上下文(阴影 DOM)中的样式。然而,添加 !important 会逆转顺序:

  1. !important 阴影上下文(最强大)
  2. !important 主上下文
  3. 普通主上下文
  4. 普通阴影上下文(最弱)

来自阴影上下文的重要样式会覆盖主文档定义的重要样式。以下是一个 odd-bird 自定义元素,其元素模板(阴影 DOM)中定义了部分样式,而主页面(轻量级 DOM)的样式表中也定义了部分样式:

两个 color 声明均为普通重要性,因此主页面中的 mediumvioletred 优先。但 font-family 声明标记了 !important,使阴影上下文中的 fantasy 优先。

重要层级

层级与来源和上下文的工作方式相同,但重要层级以相反顺序排列。唯一区别在于,层级使这种行为更加明显。

一旦开始使用级联层级,我们必须更加谨慎和有意识地使用 !important。它不再是快速跳到优先级顶端的方法,而是级联层级的一部分;是较低层级坚持某些样式至关重要的方式。

由于层级是可定制的,因此没有预定义的顺序。但我们可以想象从三个层级开始:

  1. 实用层(最强大)
  2. 组件层
  3. 默认层(最弱)

当这些层级的样式被标记为重要时,它们会生成三个新的、顺序相反的重要层级:

  1. !important 默认层(最强大)
  2. !important 组件
  3. !important 实用层
  4. 普通实用层
  5. 普通组件层
  6. 普通默认层(最弱)

在此示例中,颜色由三个普通层定义,而 utilities 层在冲突中胜出,应用了 maroon 颜色,因为 utilities 层在 @layers 中具有更高的优先级。但请注意,text-decoration 属性在 默认值组件 层中均标记为 !important,此时重要的 默认值 优先级更高,应用由 默认值 声明的下划线:

建立层级顺序

我们可以创建任意数量的层并以各种方式命名或分组。但最重要的是确保我们的层以正确的优先级顺序应用。

单个层可以在整个代码库中多次使用——层级会按其首次出现顺序堆叠。第一个遇到的层位于底部(最弱),最后一个层位于顶部(最强)。但在此之上,未分层的样式具有最高优先级

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 { a { color: yellow; } }
/* un-layered */ a { color: green; }
  1. 无层级样式(最强大)
  2. 层级-3
  3. 层级-2
  4. 层级-1(最弱)

然后,如上所述,任何重要的样式将以相反的顺序应用:

@layer layer-1 { a { color: red !important; } }
@layer layer-2 { a { color: orange !important; } }
@layer layer-3 { a { color: yellow !important; } }
/* un-layered */ a { color: green !important; }
  1. !important 层级1(最强大)
  2. !important 层级2
  3. !important 层级3
  4. !important 无层级样式
  5. 普通无层级样式
  6. 普通层级3
  7. 普通层级2
  8. 普通层级1(最弱)

层也可以分组,这使我们能够对顶级层和嵌套层进行更复杂的排序:

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 {
  @layer sub-layer-1 { a { color: yellow; } }
  @layer sub-layer-2 { a { color: green; } }
  /* un-nested */ a { color: blue; }
}
/* un-layered */ a { color: indigo; }
  1. 无层级样式(最强大)
  2. 层级-3
      1. 层级-3 无嵌套
      2. 层级-3 子层级-2
      3. 层级-3 子层级-1
  3. 层级-2
  4. 层级-1(最弱)

分组的层级在最终层级顺序中始终保持在一起(例如,层级-3的子层级将全部相邻),但这与将列表“扁平化”后的行为相同——将其转换为一个六项列表。当反转!important层级顺序时,整个扁平化列表将被反转。

但层无需在单一位置定义一次。我们为其命名,以便在某处定义层(以确定层级顺序),随后可从任何位置向其添加样式:

/* describe the layer in one place */
@layer my-layer;

/* append styles to it from anywhere */
@layer my-layer { a { color: red; } }

甚至可在单一声明中定义整个有序层级列表:

@layer one, two, three, four, five, etc;

这使得网站作者能够对层级顺序拥有最终决定权。通过在导入任何第三方代码之前提前提供层级顺序,可以在一个位置建立并重新排列顺序,无需担心第三方工具中层级的使用方式。

语法:使用级联层级

让我们看看语法!

设置顺序的 @layer 语句

由于图层按定义顺序堆叠,我们需要一个工具来在单一位置建立该顺序!

我们可以使用 @layer 语句实现这一点。语法如下:

@layer <layer-name>#;

该井号(#)表示我们可以添加任意多个层名称,以逗号分隔:

@layer reset, defaults, framework, components, utilities;

这将确定层的顺序:

  1. 未分层样式(最强大)
  2. 实用工具
  3. 组件
  4. 框架
  5. 默认值
  6. 重置(最弱)

我们可以重复此操作任意次数,但请记住:关键在于每个名称首次出现的顺序。因此以下两者效果相同:

@layer reset, defaults, framework;
@layer components, defaults, framework, reset, utilities;

排序逻辑会忽略第二个 @layer 规则中 resetdefaultsframework 的顺序,因为这些层级已经确定。此 @layer 列表语法不会为层级排序逻辑添加任何特殊机制:层级是根据 层级在代码中首次出现的顺序 堆叠的。在此示例中,reset 在第一个 @layer 列表中首次出现。任何后续的 @layer 语句只能向列表中追加层名,但无法移动已存在的层。这确保您可以始终从一个位置控制最终的整体层顺序——即样式表的开头。

这些层排序语句允许在样式表顶部使用,位于 @import 规则之前(但不能在导入之间)。我们强烈建议使用此功能在单一位置提前定义所有图层,以便您始终知道在哪里查找或进行修改。

块级 @layer 规则

块级 @layer 规则仅接受单个图层名称,但随后允许您为该图层添加样式:

@layer <layer-name> {
  /* styles added to the layer */
}

您可以在 @layer 块中放置大多数内容——媒体查询、选择器和样式、支持查询等。唯一不能放在层块中的是字符集、导入和命名空间等内容。但请放心,导入样式到层块有专门的语法。

如果层名称之前未被定义,此层规则会将其添加到层顺序中。但如果名称已定义,这允许您从文档的任何位置向现有层添加样式——而不会改变每个层的优先级。

如果我们已通过层声明规则提前定义了层顺序,我们就无需再担心这些层块的顺序:

/* establish the order up-front */
@layer defaults, components, utilities;

/* add styles to layers in any order */
@layer utilities {
  [hidden] { display: none; }
}

/* utilities will override defaults, based on established order */
@layer defaults {
  * { box-sizing: border-box; }
  img { display: block; }
}

嵌套图层的分组

可以通过嵌套图层规则对图层进行分组:

@layer one {
  /* sorting the sub-layers */
  @layer two, three;

  /* styles ... */
  @layer three { /* styles ... */ }
  @layer two { /* styles ... */ }
}

这将生成分组图层,可以通过将父图层和子图层名称用句点连接来表示。这意味着生成的子图层也可以直接从组外访问:

/* sorting nested layers directly */
@layer one.two, one.three;

/* adding to nested layers directly */
@layer one.three { /* ... */ }
@layer one.two { /* ... */ }

图层排序规则在每个嵌套级别均适用。任何未进一步嵌套的样式在该上下文中被视为“未分层”,并优先于进一步嵌套的样式:

@layer defaults {
  /* un-layered defaults (higher priority) */
  :any-link { color: rebeccapurple; }

  /* layered defaults (lower priority) */
  @layer reset {
    a[href] { color: blue; }
  }
}

分组图层也包含在其父图层中,因此图层顺序不会在不同组之间混杂。在此示例中,首先对顶级图层进行排序,然后在每个组内对图层进行排序:

@layer reset.type, default.type, reset.media, default.media;

最终的图层顺序为:

  • 未分层(最高优先级)
  • 默认组
    • 默认 未分层
    • 默认.媒体
    • 默认.类型
  • 重置组
    • 重置 未分层
    • 重置.媒体
    • 重置.类型

请注意,分层名称也具有作用域,因此它们不会与嵌套上下文之外的同名分层发生交互或冲突。两个组都可以拥有独立的 媒体 子分层。

这种分组在使用 @import<link> 导入整个样式表时尤为重要。第三方工具(如 Bootstrap)可能在内部使用分层结构——但我们可以在导入时将这些分层嵌套到一个共享的 bootstrap 分层组中,以避免潜在的分层名称冲突。

使用 @import<link> 导入整个样式表

可通过新的 layer() 函数语法结合 @import 规则将整个样式表添加到层中:

/* styles imported into to the <layer-name> layer */
@import url('example.css') layer(<layer-name>);

还有一个提案建议在 HTML <link> 元素中添加 layer 属性——尽管该功能仍在开发中,且 目前尚未被任何浏览器支持。这可用于导入第三方工具或组件库,同时将所有内部图层归类到单一图层名称下——或作为将图层组织到独立文件中的方式。

匿名(未命名)图层

图层名称有助于从多个位置访问同一图层以进行排序或合并图层块——但并非强制要求。

可通过块层规则创建匿名(未命名)层:

@layer { /* ... */ }
@layer { /* ... */ }

或使用导入语法,以layer关键字替换layer()函数:

/* styles imported into to a new anonymous layer */
@import url('../example.css') layer;

每个匿名层都是唯一的,并按其出现顺序添加到层序列中。匿名图层无法从其他图层规则中引用以进行排序或附加更多样式。

这些应尽量少用,但可能存在一些用例:

  • 项目可确保给定图层的所有样式必须位于同一位置。
  • 第三方工具可将内部图层结构隐藏在匿名图层中,使其不成为工具的公共 API 的一部分。

将值恢复到上一层

我们可以通过多种方式将样式在层叠中恢复到由较低优先级源或层定义的先前值。这包括多个现有的全局 CSS 值,以及一个新的 revert-layer 关键字,该关键字也将是全局的(适用于任何属性)。

背景:现有的全局级联关键字*

CSS 拥有多个 全局关键字,可用于任何属性以实现级联的多种回滚方式。

  • **initial** 将属性设置为在应用任何样式(包括浏览器默认值)之前的指定值。这可能令人意外,因为我们通常认为浏览器样式是初始值——但例如,displayinitial 值是 inline,无论应用于哪个元素。
  • **inherit** 将属性设置为从其父元素继承值。这是继承属性的默认行为,但仍可用于移除先前应用的值。
  • **unset** 相当于直接移除所有先前值——因此继承属性将重新采用 inherit,而非继承属性则恢复为 initial 值。
  • **revert** 仅移除我们在作者源(即网站样式)中应用的值。这是大多数情况下我们想要的,因为它允许浏览器和用户样式保持不变。

新增:revert-layer 关键字

级联层新增了一个全局 revert-layer 关键字。它与 revert 的作用相同,但仅移除我们在当前级联层中应用的值。我们可以利用它回滚级联,并使用之前层中定义的值。

在此示例中,no-theme 类会移除在 theme 层中设置的任何值。

@layer default {
  a { color: maroon; }
}

@layer theme {
  a { color: var(--brand-primary, purple); }

  .no-theme {
    color: revert-layer;
  }
}

因此,带有 .no-theme 类的链接标签将回滚到使用 default 层中设置的值。当在非分层样式中使用 revert-layer 时,它与 revert 行为相同——回滚到之前的源。

还原重要层级

如果在 revert-layer 关键字中添加 !important,情况会变得有趣。因为每个层级在层叠中都有两个不同的“正常”和“重要”位置(https://css.oddbird.net/layers/guide/#important-layers),这不仅改变了声明的优先级,还改变了被还原的层级。

假设我们定义了三个层,层栈结构如下:

  1. 实用层(最强大)
  2. 组件层
  3. 默认层(最弱)

我们可以进一步细化,不仅包括每个层的正常和重要位置,还包括未分层样式和动画:

  1. !important 默认层(最强大)
  2. !important 组件
  3. !important 实用程序
  4. !important 未分层样式
  5. CSS 动画
  6. 普通未分层样式
  7. 普通实用程序
  8. 普通组件
  9. 正常默认值(最弱)

现在,当我们在正常层(例如 utilities)中使用 revert-layer 时,结果相当直接。我们仅还原该层,而其他部分仍按正常方式应用:

  1. !important 默认值(最强大)
  2. !important 组件
  3. !important 实用程序
  4. !important 非分层样式
  5. ✅ CSS 动画
  6. ✅ 普通非分层样式
  7. ❌ 普通实用程序
  8. ✅ 普通组件
  9. ✅ 普通默认值(最弱)

但当我们将 revert-layer 移至重要位置时,我们会同时恢复普通和重要版本以及两者之间的所有内容:

  1. !important 默认值(最强大)
  2. !important 组件
  3. !important 实用程序
  4. !important 非分层样式
  5. ❌ CSS 动画
  6. ❌ 普通未分层样式
  7. ❌ 普通实用程序
  8. ✅ 普通组件
  9. ✅ 普通默认值(最弱)

使用场景:何时需要使用层级叠加?

那么,在哪些情况下我们可能会使用层级叠加?以下是一些层叠层非常有意义的示例,以及一些层叠层不适合使用的场景。

更不 intrusive 的重置和默认值

最明显的初始用例之一是创建易于覆盖的低优先级默认值。

一些重置已经通过在每个选择器周围应用 :where() 伪类来实现这一点。:where() 会移除其应用的选取器上的所有特异性,这实现了基本效果,但也有一些缺点:

  • 它必须单独应用于每个选取器
  • 重置内部的冲突必须在没有特异性的情况下解决

层允许我们更简单地包裹整个重置样式表,可以使用块 @layer 规则:

/* reset.css */
@layer reset {
  /* all reset styles in here */
}

或在导入重置时:

/* reset.css */
@import url(reset.css) layer(reset);

或两者兼用!层可以嵌套而不会改变其优先级。这样,您可以使用第三方重置,并确保它被添加到您想要的层中,无论重置样式表本身是否内部使用层。

由于分层样式优先级低于默认的“非分层”样式,这是在不重写整个 CSS 代码库的情况下开始使用级联层的好方法。

重置选择器仍保留特异性信息以帮助解决内部冲突,而无需包裹每个单独的选择器——但您同时获得了重置样式表易于覆盖的预期效果。

管理复杂的 CSS 架构

随着项目规模和复杂度的增加,定义更清晰的命名和组织 CSS 代码的边界变得有用。但 CSS 代码越多,潜在冲突的可能性就越大——尤其是来自系统不同部分的冲突,如“主题”、“组件库”或“实用类集合”。

我们不仅希望按功能对这些组件进行组织,还可根据冲突发生时系统各部分的优先级来进行分类。Harry Robert 的“倒三角 CSS”方法(https://csswizardry.com/2018/11/itcss-and-skillshare/)很好地可视化了这些层级可能包含的内容。

事实上,最初提出在CSS层叠中添加层级的建议时,ITCSS方法论被用作主要示例,并作为开发该功能的指南。

这没有特定的技术要求,但限制项目使用预定义的顶级层级集,然后根据需要扩展嵌套层级集可能更有帮助。

例如:

  1. 低级别重置和规范化样式
  2. 元素默认值,用于基本排版和可读性
  3. 主题,如浅色和深色模式
  4. 可复用模式,可能在多个组件中出现
  5. 布局和更大页面结构
  6. 单个 组件
  7. 覆盖实用工具

我们可以在CSS的开头创建这个顶级层级堆栈,只需一个层级声明:

@layer
  reset,
  default,
  themes,
  patterns,
  layouts,
  components,
  utilities;

所需的具体层级以及层级的命名方式可能因项目而异。

在此基础上,我们可以进一步细化层级结构。例如,组件内部可能包含默认样式、结构、主题和实用工具。

@layer components {
  @layer defaults, structures, themes, utilities;
}

在不改变顶层结构的前提下,我们现在可以对每个组件内的样式进行更细致的分层。

使用第三方工具和框架

在项目中集成第三方 CSS 是最常见的引发层叠冲突的场景。无论是使用共享重置样式如Normalizer或CSS Remedy,通用设计系统如Material Design,框架如Bootstrap,还是实用工具包如Tailwind——我们无法始终控制网站上使用的所有CSS的选择器特异性或重要性。有时,这甚至延伸到组织内其他地方管理的内部库、设计系统和工具。

因此,我们常常不得不围绕第三方代码来结构化内部 CSS,或者在冲突发生时通过人为提高特异性或使用 !important 标记来解决冲突。随后,我们还必须长期维护这些 hack,以适应上游的变更。

层叠层为我们提供了一种将第三方代码精确嵌入项目层叠结构中任意位置的方法——无论内部选择器如何编写。根据所用库的类型,我们可能采用多种实现方式。让我们从基础层叠堆栈开始,逐步从重置层过渡到实用层:

@layer reset, type, theme, components, utilities;

然后我们可以集成一些工具……

使用重置

如果我们使用像CSS Remedy这样的工具,我们可能还想包含一些自己的重置样式。让我们将CSS Remedy导入到reset的子层中:

@import url('remedy.css') layer(reset.remedy);

现在我们可以将自己的重置样式添加到reset层,无需进一步嵌套(除非我们需要)。由于reset层中的样式会覆盖任何进一步嵌套的样式,因此我们可以确保我们的样式在发生冲突时始终优先于CSS Remedy——无论新版本中发生了什么变化:

@import url('remedy.css') layer(reset.remedy);

@layer reset {
  :is(ol, ul)[role='list'] {
    list-style: none;
    padding-inline-start: 0;
  }
}

由于 reset 层位于样式堆栈底部,系统中其余的 CSS 将覆盖 CSS Remedy 以及我们自己的本地重置添加内容。

使用实用类

在样式堆栈的另一端,CSS 中的“实用类”可作为一种通用方式,用于实现常见模式(如为屏幕阅读器提供额外上下文)。实用类通常会打破特异性规则,因为我们希望它们定义得广泛(导致特异性较低),但我们通常也希望它们在冲突中“胜出”。

通过在层栈顶部设置一个 utilities 层,我们可以实现这一点。我们可以像重置示例一样使用它,既可以加载外部实用类到子层,也可以提供自己的:

@import url('tailwind.css') layer(utilities.tailwind);

@layer utilities {
  /* from https://kittygiraudel.com/snippets/sr-only-class/ */
  /* but with !important removed from the properties */
  .sr-only {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    -webkit-clip-path: inset(50%);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    margin: -1px;
    padding: 0;
    position: absolute;
    width: 1px;
    white-space: nowrap;
  }
}

使用设计系统和组件库

有很多CSS工具位于我们的层栈中间——结合了排版默认值、主题、组件和其他系统方面。

根据特定工具的不同,我们可能做类似于重置和实用程序示例的事情——但还有其他一些选项。一个高度集成的工具可能值得一个顶级层:

@layer reset, bootstrap, utilities;
@import url('bootstrap.css') layer(bootstrap);

如果这些工具开始在其公共 API 中提供层,我们也可以将其分解为部分——允许我们在代码中穿插库:

@import url('bootstrap/reset.css') layer(reset.bootstrap);
@import url('bootstrap/theme.css') layer(theme.bootstrap);
@import url('bootstrap/components.css') layer(components.bootstrap);

@layer theme.local {
  /* styles here will override theme.bootstrap */
  /* but not interfere with styles from components.bootstrap */
}

使用层与现有(未分层、充满 !important 的)框架

如同任何重大语言变更,当CSS层级系统广泛采用时,将有一个适应期。如果你的团队计划下个月开始使用层级,但你最喜欢的框架决定再等三年才切换到分层样式,该怎么办?许多框架仍可能比我们期望的更频繁地使用!important!在!important层级被逆转的情况下,这并不理想。

不过,分层仍能帮助我们解决问题。我们只需巧妙运用即可。我们可以决定项目所需的分层结构,这意味着可以在框架分层之上和之下添加自定义分层。

目前,我们可以使用较低分层覆盖框架的 !important 样式,使用较高分层覆盖普通样式。例如:

@layer framework.important, framework.bootstrap, framework.local;
@import url('bootstrap.css') layer(framework.bootstrap);

@layer framework.local {
  /* most of our normal framework overrides can live here */
}

@layer framework.important {
  /* add !important styles in a lower layer */
  /* to override any !important framework styles */
}

这仍然感觉像是一种权宜之计,但它有助于我们朝着更结构化的层叠方向前进。希望这只是一个临时解决方案。

设计 CSS 工具或框架

对于维护 CSS 库的人来说,层叠层可以帮助内部组织,甚至成为开发者 API 的一部分。通过为库的内部层命名,我们可以让框架用户在自定义或覆盖我们提供的样式时,能够挂接到这些层上。

例如,Bootstrap 可以暴露其“reboot”、“grid”和“utilities”层——这些层很可能按此顺序堆叠。现在用户可以决定是否将这些 Bootstrap 层加载到不同的本地层中:

@import url(bootstrap/reboot.css) layer(reset); /* reboot » reset.reboot */
@import url(bootstrap/grid.css) layer(layout); /* grid » layout.grid */
@import url(bootstrap/utils.css) layer(override); /* utils » override.utils */

或者用户可以将它们加载到一个 Bootstrap 层中,其中本地层交错其中:

@layer bs.reboot, bs.grid, bs.grid-overrides, bs.utils, bs.util-overrides;
@import url('bootstrap-all.css') layer(bs);

也可以在需要时通过将任何私有/内部层放在一个匿名(未命名的)层中来隐藏内部分层。匿名层将在遇到时添加到层顺序中,但不会暴露给用户重新排列或附加样式。

我只想让这个属性更具 !important 优先级

与部分预期相反,图层并不能轻松实现快速提升特定样式优先级以覆盖其他样式。

如果大多数样式未分层,则任何新图层相对于默认图层将被 降级。虽然可以对单个样式块进行此操作,但很快会变得难以追踪。

层的目的是建立更基础的结构,而非按样式逐一设置,而是要在整个项目中建立一致的模式。理想情况下,如果我们正确设置了层,只需将样式移动到适当(且预先定义)的层,就能得到正确的结果。

如果大多数样式已经归类到明确的层中,我们可以考虑在给定层栈的顶部添加一个新的最高权限层,或者使用未分层的样式来覆盖层。我们甚至可以考虑在层栈顶部添加一个debug层,用于在生产环境外进行探索性工作。

但动态添加新层可能会削弱该功能的组织性优势,因此应谨慎使用。最好自问:为什么这个样式要覆盖其他样式?

如果答案涉及某一类样式始终覆盖另一类样式,分层可能是合适的解决方案。这可能是因为我们正在覆盖来自我们无法控制来源的样式,或者因为我们正在编写一个工具,它应该移入我们的 utilities 层。如果答案与更具针对性的样式覆盖较不具针对性的样式有关,我们可能考虑让选择器反映这种特异性。

或者,在极少数情况下,我们可能确实有重要的样式——如果覆盖了这个特定样式,功能就无法正常工作。我们可能认为将display: none添加到[hidden]属性属于最低优先级的重置,但仍应难以覆盖。在这种情况下,!important确实是合适的工具:

@layer reset {
  [hidden] { display: none !important; }
}

样式作用域和命名空间?不!

层叠层显然是一种组织工具,它“捕获”选择器的影响,尤其是在它们冲突时。因此,乍一看,人们可能会将其视为管理作用域或命名空间的解决方案。

一个常见的直觉是为项目中的每个组件创建一层——希望这样能确保(例如).post-title仅在.post内部应用。

但层叠冲突与命名冲突不同,而层级结构并不特别适合此类作用域组织。层叠层级不会限制选择器如何匹配或应用于 HTML,仅影响它们如何层叠。因此,除非我们能确定组件 X 始终覆盖组件 Y,否则单独的组件层级并不能提供太多帮助。相反,我们需要关注正在开发的 提案 @scope 规范

将层级和组件作用域视为重叠关注点可能更有帮助:

 

作用域描述的是“我们正在样式化的对象”,而层级描述的是“我们样式化的原因”。我们还可以将层级视为“样式来源的代表”,而作用域代表“样式将应用于的对象”。

测试你的知识:哪种样式优先?

对于每种情况,假设以下段落:

<p id="intro">Hello, World!</p>

问题 1

@layer ultra-high-priority {
  #intro {
    color: red;
  }
}

p {
  color: green;
}

段落的颜色是什么?

问题 2

@layer ren, stimpy;

@layer ren {
  p { color: red !important; }
}

p { color: green; }

@layer stimpy {
  p { color: blue !important; }
}

段落的颜色是什么?

问题 3

@layer Montagues, Capulets, Verona;

@layer Montagues.Romeo { #intro { color: red; } }
@layer Montagues.Benvolio { p { color: orange; } }

@layer Capulets.Juliet { p { color: yellow; } }
@layer Verona { * { color: blue; } }
@layer Capulets.Tybalt { #intro { color: green; } }

段落的颜色是什么?

在浏览器开发者工具中调试层冲突

Chrome、Safari、Firefox 和 Edge 浏览器均提供开发者工具,可用于检查页面上某个元素所应用的样式。该元素检查器的样式面板会显示已应用的选择器,按其级联优先级排序(优先级最高者居首),随后是继承的样式。由于任何原因未应用的样式通常会以灰色显示,甚至被划掉——有时还会附带关于样式未应用的具体原因的额外信息。这是调试级联任何方面(包括层冲突)时首先要查看的地方。

Safari Technology Preview 和 Firefox Nightly 已经在此面板中显示(并排序)级联层。该工具预计将与层叠层同时在稳定版本中推出。每个选择器的层级直接显示在其上方:

Showing CSS Cascade Layers in Safari DevTools.

Safari

Showing CSS Cascade Layers in FireFox DevTools.

Firefox

Chrome/Edge 正在开发类似工具,并计划在层叠层正式发布到稳定版本时,通过 Canary(夜间构建)版本提供这些工具。我们将根据这些工具的更新和改进在此进行更新。

浏览器支持与备用方案

此浏览器支持数据来自Caniuse,其中包含更多详细信息。数字表示浏览器在该版本及以上版本中支持该功能。

桌面

Chrome Firefox IE Edge Safari
99 97 No 99 15.4

移动/平板

Android Chrome Android Firefox Android iOS Safari
138 140 138 15.4

由于层级设计旨在作为整个 CSS 架构的基础构建块,因此难以像处理其他 CSS 特性那样手动实现回退方案。回退方案可能需要复制大量代码并使用不同选择器来管理层级叠加,或提供一个更简单的回退样式表。

使用 @supports 查询特性支持

CSS 中有一个 @supports 特性,它允许作者测试对 @layer 及其他 at-规则的支持情况:

@supports at-rule(@layer) {
  /* code applied for browsers with layer support */
}

@supports not at-rule(@layer) {
  /* fallback applied for browsers without layer support */
}

然而,目前尚不清楚该查询本身何时会在浏览器中得到支持。

使用 <link> 标签在 HTML 中分配层级

目前尚无官方规范定义通过 HTML <link> 标签对整个样式表进行分层的语法,但有一个正在开发的提案。该提案包含一个新的 layer 属性,可用于将样式分配给命名或匿名层:

<!-- styles imported into to the <layer-name> layer -->
<link rel="stylesheet" href="example.css" layer="<layer-name>">

<!-- styles imported into to a new anonymous layer -->
<link rel="stylesheet" href="example.css" layer>

然而,不支持 layer 属性的旧浏览器将完全忽略该属性,并继续加载样式表而不会进行分层。结果可能相当出人意料。因此,该提案还扩展了现有的 media 属性,使其允许在 support() 函数中进行特性支持查询。

这将使我们能够根据分层功能的支持情况,条件性地创建分层链接:

<link rel="stylesheet" layer="bootstrap" media="supports(at-rule(@layer))" href="bootstrap.css">

潜在的 polyfills 和解决方法

主要浏览器均已采用 “常青”模型,通过较短的发布周期向用户推送更新。即使是 Safari,也在其看似较少的大版本更新之间,通过“补丁”更新定期发布新功能。

这意味着我们可预期浏览器对这些功能的支持将迅速提升。对于许多人而言,可能只需数月即可开始使用分层功能,无需过多顾虑旧版浏览器。

对于其他人,可能需要更长时间才能对原生浏览器支持感到放心。还有许多其他方法可以管理层叠,例如使用选择器、自定义属性和其他工具。理论上也可以模拟(或 polyfill)基本行为。有人正在开发这个 polyfill,但目前尚不清楚何时能完成。

更多资源

CSS 层叠层仍在不断发展,但已经有很多资源,包括文档、文章、视频和示例,帮助你更深入地了解层叠层及其工作原理。

参考资料

文章

视频

示例

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注